// Copyright 2025 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.

#pragma once

#include <zephyr/device.h>
#include <zephyr/drivers/gpio.h>

#include <cstdint>

#include "pw_assert/assert.h"
#include "pw_digital_io/digital_io.h"

/// @brief Callback handler used by all pw::digital_io::ZephyrDigital*Interrupt
///
/// @param dev Zephyr device pointer of the interrupt controller
/// @param cb Callback data structure registered with Zephyr
/// @param pins The pins that triggered the callback
extern "C" void pw_digital_io_ZephyrCallbackHandler(const struct device* dev,
                                                    struct gpio_callback* cb,
                                                    uint32_t pins);

namespace pw::digital_io {

/// @brief Convert Pigweed InterruptTrigger to Zephyr gpio flags
///
/// @param trigger The Pigweed trigger to convert
/// @return Corresponding Zephyr gpio_flags_t
constexpr gpio_flags_t InterruptTriggerToZephyrFlags(InterruptTrigger trigger) {
  switch (trigger) {
    case InterruptTrigger::kActivatingEdge:
      return GPIO_INT_EDGE_TO_ACTIVE;
    case InterruptTrigger::kDeactivatingEdge:
      return GPIO_INT_EDGE_TO_INACTIVE;
    case InterruptTrigger::kBothEdges:
      return GPIO_INT_EDGE_BOTH;
  }

  PW_UNREACHABLE;
}

/// @brief Wrapper for Zephyr callbacks
///
/// Used to map the Zephyr interrupt callback function to the Pigweed
/// InterruptHandler
struct gpio_callback_and_handler {
  struct gpio_callback data;  //< Zephyr gpio_callback data structure
  InterruptHandler handler;   //< Pigweed InterruptHandler to call
};

/// @brief Construct a bridge between Zephyr's gpio API and Pigweed's
///
/// This is a catch all class that will be used to implement the various Pigweed
/// Digital* classes.
///
class GenericZephyrDigitalInOut {
 public:
  enum class InterruptSupport : bool { kNotAllowed = false, kAllowed = true };

  /// @brief Construct a generic Digital I/O around a Zephyr devicetree spec
  ///
  /// @param dt_spec The spec to use, usually from DPIO_DT_SPEC_GET()
  /// @param flags Flags used to configure the GPIO
  /// @param supported Allow pin to be configured as an interrupt
  GenericZephyrDigitalInOut(const struct gpio_dt_spec dt_spec,
                            gpio_flags_t flags,
                            InterruptSupport supported)
      : gpio_spec_(dt_spec),
        flags_(flags),
        interrupt_supported_(supported),
        trigger_(InterruptTrigger::kActivatingEdge) {
    if (interrupt_supported_ == InterruptSupport::kAllowed) {
      // We're using interrupts, init the callback object
      gpio_init_callback(&callback_.data,
                         pw_digital_io_ZephyrCallbackHandler,
                         BIT(gpio_spec_.pin));
    }
  }

 protected:
  Status DoEnable(bool enable) {
    // Select the right flags based on 'enable'
    // Make sure GPIO interrupt configuration flags aren't set since that is
    // handled by gpio_pin_interrupt_configure_dt()
    PW_DASSERT((flags_ & GPIO_INT_MASK) == 0);
    gpio_flags_t flags = enable ? flags_ : GPIO_DISCONNECTED;

    // Configure the pin
    return gpio_pin_configure_dt(&gpio_spec_, flags) == 0
               ? pw::OkStatus()
               : pw::Status::Internal();
  }

  Result<State> DoGetState() {
    // Check pin has been configured to support GPIO_INPUT
    PW_DASSERT(flags_ & GPIO_INPUT);

    // Verify the device is ready
    int rc = gpio_is_ready_dt(&gpio_spec_);
    if (rc == 0) {
      return pw::Status::Unavailable();
    }
    // Get the current pin state
    rc = gpio_pin_get_dt(&gpio_spec_);
    return Result<State>(rc == 0 ? State::kInactive : State::kActive);
  }

  // Only enable if flags_ include GPIO_OUTPUT
  Status DoSetState(State state) {
    // Check pin has been configured to support GPIO_OUTPUT
    PW_DASSERT(flags_ & GPIO_OUTPUT);

    // Verify the device is ready
    int rc = gpio_is_ready_dt(&gpio_spec_);
    if (rc == 0) {
      return pw::Status::Unavailable();
    }

    // Set the pin state
    rc = gpio_pin_set_dt(&gpio_spec_, state == State::kActive ? 1 : 0);
    return rc == 0 ? pw::OkStatus() : pw::Status::Internal();
  }

  // Only enable if InterruptSupport::kAllowed is set
  Status DoSetInterruptHandler(InterruptTrigger trigger,
                               InterruptHandler&& handler) {
    // Check if pin supports interrupts
    PW_DASSERT(interrupt_supported_ == InterruptSupport::kAllowed);

    // Verify the device is ready
    int rc = gpio_is_ready_dt(&gpio_spec_);
    if (rc == 0) {
      return pw::Status::Unavailable();
    }

    // Save the handler
    callback_.handler = std::move(handler);

    if (callback_.handler == nullptr) {
      // Remove the callbacks to disable
      return gpio_remove_callback_dt(&gpio_spec_, &callback_.data) == 0
                 ? pw::OkStatus()
                 : pw::Status::Internal();
    }

    // Store the trigger setting to be used by DoEnableInterruptHandler
    trigger_ = trigger;

    // Add the callbacks to enable
    return gpio_add_callback_dt(&gpio_spec_, &callback_.data) == 0
               ? pw::OkStatus()
               : pw::Status::Internal();
  }

  // Only enable if InterruptSupport::kAllowed is set
  Status DoEnableInterruptHandler(bool enable) {
    // Check if pin supports interrupts
    PW_DASSERT(interrupt_supported_ == InterruptSupport::kAllowed);

    // If we don't have a handler, we can't enable
    if (enable && callback_.handler == nullptr) {
      return pw::Status::FailedPrecondition();
    }

    gpio_flags_t interrupt_flags =
        enable ? InterruptTriggerToZephyrFlags(trigger_) : GPIO_INT_DISABLE;

    // Configure the interrupt trigger
    return gpio_pin_interrupt_configure_dt(&gpio_spec_, interrupt_flags) == 0
               ? pw::OkStatus()
               : pw::Status::Internal();
  }

 private:
  const struct gpio_dt_spec gpio_spec_;
  gpio_flags_t flags_;
  InterruptSupport interrupt_supported_;
  InterruptTrigger trigger_;
  struct gpio_callback_and_handler callback_;
};

/// @brief Zephyr wrapper for pw::digital_io::DigitalIn
class ZephyrDigitalIn : public DigitalIn, public GenericZephyrDigitalInOut {
 public:
  /// @brief Construct a ZephyrDigitalIn
  ///
  /// @param dt_spec The spec to use, usually from DPIO_DT_SPEC_GET()
  /// @param extra_flags Additional flags used to configure the GPIO
  ZephyrDigitalIn(const struct gpio_dt_spec dt_spec,
                  gpio_flags_t extra_flags = 0)
      : GenericZephyrDigitalInOut(
            dt_spec, GPIO_INPUT | extra_flags, InterruptSupport::kNotAllowed) {}

 private:
  Result<State> DoGetState() override {
    return GenericZephyrDigitalInOut::DoGetState();
  }

  Status DoEnable(bool enable) override {
    return GenericZephyrDigitalInOut::DoEnable(enable);
  }
};

/// @brief Zephyr wrapper for pw::digital_io::DigitalInInterrupt
class ZephyrDigitalInInterrupt : public DigitalInInterrupt,
                                 public GenericZephyrDigitalInOut {
 public:
  /// @brief Construct a ZephyrDigitalInInterrupt
  ///
  /// @param dt_spec The spec to use, usually from DPIO_DT_SPEC_GET()
  /// @param extra_flags Additional flags used to configure the GPIO
  ZephyrDigitalInInterrupt(const struct gpio_dt_spec dt_spec,
                           gpio_flags_t extra_flags = 0)
      : GenericZephyrDigitalInOut(
            dt_spec, GPIO_INPUT | extra_flags, InterruptSupport::kAllowed) {}

 private:
  Result<State> DoGetState() override {
    return GenericZephyrDigitalInOut::DoGetState();
  }

  Status DoSetInterruptHandler(InterruptTrigger trigger,
                               InterruptHandler&& handler) override {
    return GenericZephyrDigitalInOut::DoSetInterruptHandler(trigger,
                                                            std::move(handler));
  }

  Status DoEnableInterruptHandler(bool enable) override {
    return GenericZephyrDigitalInOut::DoEnableInterruptHandler(enable);
  }

  Status DoEnable(bool enable) override {
    return GenericZephyrDigitalInOut::DoEnable(enable);
  }
};

/// @brief Zephyr wrapper for pw::digital_io::DigitalInOut
class ZephyrDigitalInOut : public DigitalInOut,
                           public GenericZephyrDigitalInOut {
 public:
  /// @brief Construct a ZephyrDigitalInOut
  ///
  /// @param dt_spec The spec to use, usually from DPIO_DT_SPEC_GET()
  /// @param extra_flags Additional flags used to configure the GPIO
  ZephyrDigitalInOut(const struct gpio_dt_spec dt_spec,
                     gpio_flags_t extra_flags = 0)
      : GenericZephyrDigitalInOut(dt_spec,
                                  GPIO_INPUT | GPIO_OUTPUT | extra_flags,
                                  InterruptSupport::kNotAllowed) {}

 private:
  Result<State> DoGetState() override {
    return GenericZephyrDigitalInOut::DoGetState();
  }

  Status DoSetState(State state) override {
    return GenericZephyrDigitalInOut::DoSetState(state);
  }

  Status DoEnable(bool enable) override {
    return GenericZephyrDigitalInOut::DoEnable(enable);
  }
};

/// @brief Zephyr wrapper for pw::digital_io::DigitalOutInterrupt
class ZephyrDigitalInOutInterrupt : public DigitalInOutInterrupt,
                                    public GenericZephyrDigitalInOut {
 public:
  /// @brief Construct a ZephyrDigitalInOutInterrupt
  ///
  /// @param dt_spec The spec to use, usually from DPIO_DT_SPEC_GET()
  /// @param extra_flags Additional flags used to configure the GPIO
  ZephyrDigitalInOutInterrupt(const struct gpio_dt_spec dt_spec,
                              gpio_flags_t extra_flags = 0)
      : GenericZephyrDigitalInOut(dt_spec,
                                  GPIO_INPUT | GPIO_OUTPUT | extra_flags,
                                  InterruptSupport::kAllowed) {}

 private:
  Result<State> DoGetState() override {
    return GenericZephyrDigitalInOut::DoGetState();
  }

  Status DoSetState(State state) override {
    return GenericZephyrDigitalInOut::DoSetState(state);
  }

  Status DoSetInterruptHandler(InterruptTrigger trigger,
                               InterruptHandler&& handler) override {
    return GenericZephyrDigitalInOut::DoSetInterruptHandler(trigger,
                                                            std::move(handler));
  }

  Status DoEnableInterruptHandler(bool enable) override {
    return GenericZephyrDigitalInOut::DoEnableInterruptHandler(enable);
  }

  Status DoEnable(bool enable) override {
    return GenericZephyrDigitalInOut::DoEnable(enable);
  }
};

/// @brief Zephyr wrapper for pw::digital_io::DigitalInterrupt
class ZephyrDigitalInterrupt : public DigitalInterrupt,
                               public GenericZephyrDigitalInOut {
 public:
  /// @brief Construct a ZephyrDigitalInterrupt
  ///
  /// @param dt_spec The spec to use, usually from DPIO_DT_SPEC_GET()
  /// @param extra_flags Additional flags used to configure the GPIO
  ZephyrDigitalInterrupt(const struct gpio_dt_spec dt_spec,
                         gpio_flags_t extra_flags = 0)
      : GenericZephyrDigitalInOut(
            dt_spec, GPIO_INPUT | extra_flags, InterruptSupport::kAllowed) {}

 private:
  Status DoSetInterruptHandler(InterruptTrigger trigger,
                               InterruptHandler&& handler) override {
    return GenericZephyrDigitalInOut::DoSetInterruptHandler(trigger,
                                                            std::move(handler));
  }

  Status DoEnableInterruptHandler(bool enable) override {
    return GenericZephyrDigitalInOut::DoEnableInterruptHandler(enable);
  }

  Status DoEnable(bool enable) override {
    return GenericZephyrDigitalInOut::DoEnable(enable);
  }
};

/// @brief Zephyr wrapper for pw::digital_io::DigitalOut
class ZephyrDigitalOut : public DigitalOut, public GenericZephyrDigitalInOut {
 public:
  /// @brief Construct a ZephyrDigitalOut
  ///
  /// @param dt_spec The spec to use, usually from DPIO_DT_SPEC_GET()
  /// @param extra_flags Additional flags used to configure the GPIO
  ZephyrDigitalOut(const struct gpio_dt_spec dt_spec,
                   gpio_flags_t extra_flags = 0)
      : GenericZephyrDigitalInOut(
            dt_spec, GPIO_OUTPUT | extra_flags, InterruptSupport::kNotAllowed) {
  }

 private:
  Status DoSetState(State state) override {
    return GenericZephyrDigitalInOut::DoSetState(state);
  }

  Status DoEnable(bool enable) override {
    return GenericZephyrDigitalInOut::DoEnable(enable);
  }
};

/// @brief Zephyr wrapper for pw::digital_io::DigitalOutInterrupt
class ZephyrDigitalOutInterrupt : public DigitalOutInterrupt,
                                  public GenericZephyrDigitalInOut {
 public:
  /// @brief Construct a ZephyrDigitalOutInterrupt
  ///
  /// @param dt_spec The spec to use, usually from DPIO_DT_SPEC_GET()
  /// @param extra_flags Additional flags used to configure the GPIO
  ZephyrDigitalOutInterrupt(const struct gpio_dt_spec dt_spec,
                            gpio_flags_t extra_flags = 0)
      : GenericZephyrDigitalInOut(dt_spec,
                                  GPIO_INPUT | GPIO_OUTPUT | extra_flags,
                                  InterruptSupport::kAllowed) {}

 private:
  Status DoSetState(State state) override {
    return GenericZephyrDigitalInOut::DoSetState(state);
  }

  Status DoSetInterruptHandler(InterruptTrigger trigger,
                               InterruptHandler&& handler) override {
    return GenericZephyrDigitalInOut::DoSetInterruptHandler(trigger,
                                                            std::move(handler));
  }

  Status DoEnableInterruptHandler(bool enable) override {
    return GenericZephyrDigitalInOut::DoEnableInterruptHandler(enable);
  }

  Status DoEnable(bool enable) override {
    return GenericZephyrDigitalInOut::DoEnable(enable);
  }
};

}  // namespace pw::digital_io
